package dslab.naming;

import dslab.ComponentFactory;
import dslab.Constants;
import dslab.TestInputStream;
import dslab.TestOutputStream;
import dslab.nameserver.AlreadyRegisteredException;
import dslab.nameserver.INameserverRemote;
import dslab.util.Config;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.junit.Before;
import org.junit.FixMethodOrder;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ErrorCollector;
import org.junit.runners.MethodSorters;

import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.nullValue;

@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class AdvancedNameserverTest {
    private static final Log LOG = LogFactory.getLog(AdvancedNameserverTest.class);

    @Rule
    public ErrorCollector err = new ErrorCollector();

    // ns-root "." data
    private Config nsRootConfig;
    private TestInputStream nsRootIn;
    private TestOutputStream nsRootOut;

    // ns-planet "planet." data
    private Config nsPlanetConfig;
    private TestInputStream nsPlanetIn;
    private TestOutputStream nsPlanetOut;

    // ns-ze "ze." data
    private Config nsZeConfig;
    private TestInputStream nsZeIn;
    private TestOutputStream nsZeOut;

    // ns-earth-planet "earth.planet." data
    private Config nsEarthPlanetConfig;
    private TestInputStream nsEarthPlanetIn;
    private TestOutputStream nsEarthPlanetOut;

    private static void sendShutdown(TestInputStream in) {
        try {
            LOG.info("Sending shutdown to component component");
            in.addLine("shutdown");
            Thread.sleep(Constants.COMPONENT_TEARDOWN_WAIT);
        } catch (InterruptedException e) {
            LOG.error("Interrupted while waiting on component teardown");
            throw new RuntimeException(e);
        }
    }

    private void shutdownAll() {
        sendShutdown(nsEarthPlanetIn);
        sendShutdown(nsZeIn);
        sendShutdown(nsPlanetIn);
        sendShutdown(nsRootIn);
    }

    private void startRoot() throws Exception {
        LOG.info("Starting ns-root thread");
        Runnable nsRoot = ComponentFactory.createNameserver("ns-root", nsRootIn, nsRootOut);
        Thread nsRootThread = new Thread(nsRoot);
        nsRootThread.start();
        Thread.sleep(Constants.COMPONENT_STARTUP_WAIT);
    }

    private void startPlanet() throws Exception {
        LOG.info("Starting ns-planet thread");
        Runnable nsPlanet = ComponentFactory.createNameserver("ns-planet", nsPlanetIn, nsPlanetOut);
        Thread nsPlanetThread = new Thread(nsPlanet);
        nsPlanetThread.start();
        Thread.sleep(Constants.COMPONENT_STARTUP_WAIT);
    }

    private void startZe() throws Exception {
        LOG.info("Starting ns-ze thread");
        Runnable nsZe = ComponentFactory.createNameserver("ns-ze", nsZeIn, nsZeOut);
        Thread nsZeThread = new Thread(nsZe);
        nsZeThread.start();
        Thread.sleep(Constants.COMPONENT_STARTUP_WAIT);
    }

    private void startEarthPlanet() throws Exception {
        LOG.info("Starting ns-earth-planet thread");
        Runnable nsEarthPlanet = ComponentFactory.createNameserver("ns-earth-planet", nsEarthPlanetIn, nsEarthPlanetOut);
        Thread nsEarthPlanetThread = new Thread(nsEarthPlanet);
        nsEarthPlanetThread.start();
        Thread.sleep(Constants.COMPONENT_STARTUP_WAIT);
    }

    private void startAll() throws Exception {
        startRoot();
        startPlanet();
        startZe();
        startEarthPlanet();
    }

    @Before
    public void setUp() throws Exception {
        // initialize TestInputStreams
        this.nsRootIn = new TestInputStream();
        this.nsPlanetIn = new TestInputStream();
        this.nsZeIn = new TestInputStream();
        this.nsEarthPlanetIn = new TestInputStream();

        // initialize TestOutputStreams
        this.nsRootOut = new TestOutputStream();
        this.nsPlanetOut = new TestOutputStream();
        this.nsZeOut = new TestOutputStream();
        this.nsEarthPlanetOut = new TestOutputStream();

        // initialize configs
        this.nsRootConfig = new Config("ns-root");
        this.nsPlanetConfig = new Config("ns-planet");
        this.nsZeConfig = new Config("ns-ze");
        this.nsEarthPlanetConfig = new Config("ns-earth-planet");
    }

    @Test(timeout = 30000)
    public void startAndShutdownAllNameservers_checkEachForExistenceBeforeAndAfterShutdown() throws Exception {
        startAll();

        try {
            Registry registry = LocateRegistry.getRegistry(
                    nsRootConfig.getString("registry.host"),
                    nsRootConfig.getInt("registry.port")
            );
            String id = nsRootConfig.getString("root_id");

            LOG.info("Retrieving ns-root from registry with `lookup`");
            INameserverRemote root = (INameserverRemote) registry.lookup(id);

            LOG.info("Retrieving ns-planet from ns-root with `getNameserver`");
            INameserverRemote planet = root.getNameserver("planet");

            LOG.info("Retrieving ns-ze from ns-root with `getNameserver`");
            INameserverRemote ze = root.getNameserver("ze");

            LOG.info("Retrieving ns-earth-planet from ns-earth with `getNameserver`");
            INameserverRemote earth = planet.getNameserver("earth");

            // ns-earth-planet
            {
                LOG.info("Check if ns-earth-planet is functioning");
                err.checkThat(
                        "ns-earth-planet should return null on empty lookup",
                        earth.lookup(""),
                        nullValue()
                );

                LOG.info("Shutdown ns-earth-planet");
                sendShutdown(nsEarthPlanetIn);

                LOG.info("Check that ns-earth-planet does not exist");
                try {
                    earth.lookup("");
                    err.addError(new AssertionError("ns-earth-planet should not exist"));
                } catch (RemoteException ignored) {
                }
            }

            // ns-ze
            {
                LOG.info("Check if ns-ze is functioning");
                err.checkThat(
                        "ns-ze should return null on empty lookup",
                        ze.lookup(""),
                        nullValue()
                );

                LOG.info("Shutdown ns-ze");
                sendShutdown(nsZeIn);

                LOG.info("Check that ns-ze does not exist");
                try {
                    ze.lookup("");
                    err.addError(new AssertionError("ns-ze should not exist"));
                } catch (RemoteException ignored) {
                }
            }

            // ns-planet
            {
                LOG.info("Check if ns-planet is functioning");
                err.checkThat(
                        "ns-planet should return null on empty lookup",
                        planet.lookup(""),
                        nullValue()
                );

                LOG.info("Shutdown ns-planet");
                sendShutdown(nsPlanetIn);

                LOG.info("Check that ns-planet does not exist");
                try {
                    planet.lookup("");
                    err.addError(new AssertionError("ns-planet should not exist"));
                } catch (RemoteException ignored) {
                }
            }

            // ns-root
            {
                LOG.info("Check if ns-root is functioning");
                err.checkThat(
                        "ns-root should return null on empty lookup",
                        root.lookup(""),
                        nullValue()
                );

                LOG.info("Shutdown ns-root");
                sendShutdown(nsRootIn);

                LOG.info("Check that ns-root does not exist");
                try {
                    root.lookup("");
                    err.addError(new AssertionError("ns-root should not exist"));
                } catch (RemoteException ignored) {
                }
            }
        } catch (Exception e) {
            err.addError(new AssertionError("unexpected error occured", e));
        }
    }

    @Test(timeout = 30000)
    public void registerMailboxes_shouldBeRetrievableWithLookupDomain() throws Exception {
        startRoot();

        try {
            Registry registry = LocateRegistry.getRegistry(
                    nsRootConfig.getString("registry.host"),
                    nsRootConfig.getInt("registry.port")
            );
            String id = nsRootConfig.getString("root_id");

            INameserverRemote root = (INameserverRemote) registry.lookup(id);

            String A = "1.1.1.1:1";
            String B = "2.2.2.2:2";
            String C = "3.3.3.3:3";

            root.registerMailboxServer("a", A);
            root.registerMailboxServer("b", B);
            root.registerMailboxServer("c", C);

            err.checkThat(
                    "lookupDomain of 'a' should return correct address",
                    root.lookupDomain("a"),
                    is(A)
            );
            err.checkThat(
                    "lookupDomain of 'b' should return correct address",
                    root.lookupDomain("b"),
                    is(B)
            );
            err.checkThat(
                    "lookupDomain of 'c' should return correct address",
                    root.lookupDomain("c"),
                    is(C)
            );
        } catch (Exception e) {
            err.addError(new AssertionError("unkown exception thrown", e));
        } finally {
            sendShutdown(nsRootIn);
        }
    }

    @Test(timeout = 30000)
    public void registerDeepMailboxes_shouldBeRetrievableFromReleavantNameservers() throws Exception {
        startRoot();
        startPlanet();

        try {
            Registry registry = LocateRegistry.getRegistry(
                    nsRootConfig.getString("registry.host"),
                    nsRootConfig.getInt("registry.port")
            );
            String id = nsRootConfig.getString("root_id");

            INameserverRemote root = (INameserverRemote) registry.lookup(id);
            INameserverRemote planet = root.getNameserver("planet");

            String address = "1.2.3.4:5";
            planet.registerMailboxServer("test", address);

            err.checkThat(
                    "ns-planet should be able to look up mailbox server",
                    planet.lookupDomain("test.planet"),
                    is(address)
            );

            err.checkThat(
                    "ns-root should be able to look up mailbox server",
                    root.lookupDomain("test.planet"),
                    is(address)
            );

        } catch (Exception e) {
            err.addError(new AssertionError("unkown exception thrown", e));
        } finally {
            sendShutdown(nsPlanetIn);
            sendShutdown(nsRootIn);
        }
    }

    @Test(timeout = 30000)
    public void registerIndirectlyEqualMailboxServer_shouldThrowAlreadyRegisteredException() throws Exception {
        startRoot();
        startZe();

        try {
            Registry registry = LocateRegistry.getRegistry(
                    nsRootConfig.getString("registry.host"),
                    nsRootConfig.getInt("registry.port")
            );
            String id = nsRootConfig.getString("root_id");

            INameserverRemote root = (INameserverRemote) registry.lookup(id);
            INameserverRemote ze = root.getNameserver("ze");

            root.registerMailboxServer("test1.ze", "");

            try {
                ze.registerMailboxServer("test1", "");
                err.addError(new AssertionError("ns-ze should have thrown AlreadyRegisteredException"));
            } catch (AlreadyRegisteredException ignored) {
            }

            ze.registerMailboxServer("test2", "");

            try {
                root.registerMailboxServer("test2.ze", "");
                err.addError(new AssertionError("ns-root should have thrown AlreadyRegisteredException"));
            } catch (AlreadyRegisteredException ignored) {
            }
        } catch (Exception e) {
            err.addError(new AssertionError("unkown exception thrown", e));
        } finally {
            sendShutdown(nsZeIn);
            sendShutdown(nsRootIn);
        }
    }
}
